package types

import "strings"

// Ref makes a reference to the given type. It can only be used for e.g.
// passing to namers.
func Ref(packageName, typeName string) *Type {
	return &Type{Name: Name{
		Name:    typeName,
		Package: packageName,
	}}
}

// Name is a type name may have a package qualifier.
type Name struct {
	// Empty if embedded or builtin. This is the package path unless Path is specified.
	Package string
	// The type name.
	Name string
	// An optional location of the type definition for languages that can have disjoint
	// packages and paths.
	Path string
}

// String returns the name formatted as a string.
func (n Name) String() string {
	if n.Package == "" {
		return n.Name
	}
	return n.Package + "." + n.Name
}

// ParseFullyQualifiedName parses a name like k8s.io/kubernetes/pkg/api.Pod into a Name.
func ParseFullyQualifiedName(fqn string) Name {
	cs := strings.Split(fqn, ".")
	pkg := ""
	if len(cs) > 1 {
		pkg = strings.Join(cs[0:len(cs)-1], ".")
	}
	return Name{
		Name:    cs[len(cs)-1],
		Package: pkg,
	}
}

// Kind is the possible classes of types.
type Kind string

// consts
const (
	// Builtin is a primitive, like bool, string, int.
	Builtin Kind = "Builtin"
	Struct  Kind = "Struct"
	Map     Kind = "Map"
	Slice   Kind = "Slice"
	Pointer Kind = "Pointer"

	// Alias is an alias of another type, e.g. in:
	//   type Foo string
	//   type Bar Foo
	// Bar is an alias of Foo.
	//
	// In the real go type system, Foo is a "Named" string; but to simplify
	// generation, this type system will just say that Foo *is* a builtin.
	// We then need "Alias" as a way for us to say that Bar *is* a Foo.
	Alias Kind = "Alias"

	// Interface is any type that could have differing types at run time.
	Interface Kind = "Interface"

	// The remaining types are included for completeness, but are not well
	// supported.
	Array Kind = "Array" // Array is just like slice, but has a fixed length.
	Chan  Kind = "Chan"
	Func  Kind = "Func"

	// DeclarationOf is different from other Kinds; it indicates that instead of
	// representing an actual Type, the type is a declaration of an instance of
	// a type. E.g., a top-level function, variable, or constant. See the
	// comment for Type.Name for more detail.
	DeclarationOf Kind = "DeclarationOf"
	Unknown       Kind = ""
	Unsupported   Kind = "Unsupported"

	// Protobuf is protobuf type.
	Protobuf Kind = "Protobuf"
)

// Package holds package-level information.
// Fields are public, as everything in this package, to enable consumption by
// templates (for example). But it is strongly encouraged for code to build by
// using the provided functions.
type Package struct {
	// Canonical name of this package-- its path.
	Path string

	// The location this package was loaded from
	SourcePath string

	// Short name of this package; the name that appears in the
	// 'package x' line.
	Name string

	// The comment right above the package declaration in doc.go, if any.
	DocComments []string

	// All comments from doc.go, if any.
	// TODO: remove Comments and use DocComments everywhere.
	Comments []string

	// Types within this package, indexed by their name (*not* including
	// package name).
	Types map[string]*Type

	// Functions within this package, indexed by their name (*not* including
	// package name).
	Functions map[string]*Type

	// Global variables within this package, indexed by their name (*not* including
	// package name).
	Variables map[string]*Type

	// Packages imported by this package, indexed by (canonicalized)
	// package path.
	Imports map[string]*Package
}

// Has returns true if the given name references a type known to this package.
func (p *Package) Has(name string) bool {
	_, has := p.Types[name]
	return has
}

// Type gets the given Type in this Package.  If the Type is not already
// defined, this will add it and return the new Type value.  The caller is
// expected to finish initialization.
func (p *Package) Type(typeName string) *Type {
	if t, ok := p.Types[typeName]; ok {
		return t
	}
	if p.Path == "" {
		// Import the standard builtin types!
		if t, ok := builtins.Types[typeName]; ok {
			p.Types[typeName] = t
			return t
		}
	}
	t := &Type{Name: Name{Package: p.Path, Name: typeName}}
	p.Types[typeName] = t
	return t
}

// Function gets the given function Type in this Package. If the function is
// not already defined, this will add it.  If a function is added, it's the
// caller's responsibility to finish construction of the function by setting
// Underlying to the correct type.
func (p *Package) Function(funcName string) *Type {
	if t, ok := p.Functions[funcName]; ok {
		return t
	}
	t := &Type{Name: Name{Package: p.Path, Name: funcName}}
	t.Kind = DeclarationOf
	p.Functions[funcName] = t
	return t
}

// Variable gets the given variable Type in this Package. If the variable is
// not already defined, this will add it. If a variable is added, it's the caller's
// responsibility to finish construction of the variable by setting Underlying
// to the correct type.
func (p *Package) Variable(varName string) *Type {
	if t, ok := p.Variables[varName]; ok {
		return t
	}
	t := &Type{Name: Name{Package: p.Path, Name: varName}}
	t.Kind = DeclarationOf
	p.Variables[varName] = t
	return t
}

// HasImport returns true if p imports packageName. Package names include the
// package directory.
func (p *Package) HasImport(packageName string) bool {
	_, has := p.Imports[packageName]
	return has
}

// Universe is a map of all packages. The key is the package name, but you
// should use Package(), Type(), Function(), or Variable() instead of direct
// access.
type Universe map[string]*Package

// Type returns the canonical type for the given fully-qualified name. Builtin
// types will always be found, even if they haven't been explicitly added to
// the map. If a non-existing type is requested, this will create (a marker for)
// it.
func (u Universe) Type(n Name) *Type {
	return u.Package(n.Package).Type(n.Name)
}

// Function returns the canonical function for the given fully-qualified name.
// If a non-existing function is requested, this will create (a marker for) it.
// If a marker is created, it's the caller's responsibility to finish
// construction of the function by setting Underlying to the correct type.
func (u Universe) Function(n Name) *Type {
	return u.Package(n.Package).Function(n.Name)
}

// Variable returns the canonical variable for the given fully-qualified name.
// If a non-existing variable is requested, this will create (a marker for) it.
// If a marker is created, it's the caller's responsibility to finish
// construction of the variable by setting Underlying to the correct type.
func (u Universe) Variable(n Name) *Type {
	return u.Package(n.Package).Variable(n.Name)
}

// AddImports registers import lines for packageName. May be called multiple times.
// You are responsible for canonicalizing all package paths.
func (u Universe) AddImports(packagePath string, importPaths ...string) {
	p := u.Package(packagePath)
	for _, i := range importPaths {
		p.Imports[i] = u.Package(i)
	}
}

// Package returns the Package for the given path.
// If a non-existing package is requested, this will create (a marker for) it.
// If a marker is created, it's the caller's responsibility to finish
// construction of the package.
func (u Universe) Package(packagePath string) *Package {
	if p, ok := u[packagePath]; ok {
		return p
	}
	p := &Package{
		Path:      packagePath,
		Types:     map[string]*Type{},
		Functions: map[string]*Type{},
		Variables: map[string]*Type{},
		Imports:   map[string]*Package{},
	}
	u[packagePath] = p
	return p
}

// Type represents a subset of possible go types.
type Type struct {
	// There are two general categories of types, those explicitly named
	// and those anonymous. Named ones will have a non-empty package in the
	// name field.
	//
	// An exception: If Kind == DeclarationOf, then this name is the name of a
	// top-level function, variable, or const, and the type can be found in Underlying.
	// We do this to allow the naming system to work against these objects, even
	// though they aren't strictly speaking types.
	Name Name

	// The general kind of this type.
	Kind Kind

	// If there are comment lines immediately before the type definition,
	// they will be recorded here.
	CommentLines []string

	// If there are comment lines preceding the `CommentLines`, they will be
	// recorded here. There are two cases:
	// ---
	// SecondClosestCommentLines
	// a blank line
	// CommentLines
	// type definition
	// ---
	//
	// or
	// ---
	// SecondClosestCommentLines
	// a blank line
	// type definition
	// ---
	SecondClosestCommentLines []string

	// If Kind == Struct
	Members []Member

	// If Kind == Map, Slice, Pointer, or Chan
	Elem *Type

	// If Kind == Map, this is the map's key type.
	Key *Type

	// If Kind == Alias, this is the underlying type.
	// If Kind == DeclarationOf, this is the type of the declaration.
	Underlying *Type

	// If Kind == Interface, this is the set of all required functions.
	// Otherwise, if this is a named type, this is the list of methods that
	// type has. (All elements will have Kind=="Func")
	Methods map[string]*Type

	// If Kind == func, this is the signature of the function.
	Signature *Signature

	// TODO: Add:
	// * channel direction
	// * array length
}

// String returns the name of the type.
func (t *Type) String() string {
	return t.Name.String()
}

// IsPrimitive returns whether the type is a built-in type or is an alias to a
// built-in type.  For example: strings and aliases of strings are primitives,
// structs are not.
func (t *Type) IsPrimitive() bool {
	if t.Kind == Builtin || (t.Kind == Alias && t.Underlying.Kind == Builtin) {
		return true
	}
	return false
}

// IsAssignable returns whether the type is deep-assignable.  For example,
// slices and maps and pointers are shallow copies, but ints and strings are
// complete.
func (t *Type) IsAssignable() bool {
	if t.IsPrimitive() {
		return true
	}
	if t.Kind == Struct {
		for _, m := range t.Members {
			if !m.Type.IsAssignable() {
				return false
			}
		}
		return true
	}
	return false
}

// IsAnonymousStruct returns true if the type is an anonymous struct or an alias
// to an anonymous struct.
func (t *Type) IsAnonymousStruct() bool {
	return (t.Kind == Struct && t.Name.Name == "struct{}") || (t.Kind == Alias && t.Underlying.IsAnonymousStruct())
}

// Member is a single struct member
type Member struct {
	// The name of the member.
	Name string

	// If the member is embedded (anonymous) this will be true, and the
	// Name will be the type name.
	Embedded bool

	// If there are comment lines immediately before the member in the type
	// definition, they will be recorded here.
	CommentLines []string

	// If there are tags along with this member, they will be saved here.
	Tags string

	// The type of this member.
	Type *Type
}

// String returns the name and type of the member.
func (m Member) String() string {
	return m.Name + " " + m.Type.String()
}

// Signature is a function's signature.
type Signature struct {
	// TODO: store the parameter names, not just types.

	// If a method of some type, this is the type it's a member of.
	Receiver   *Type
	Parameters []*Type
	Results    []*Type

	// True if the last in parameter is of the form ...T.
	Variadic bool

	// If there are comment lines immediately before this
	// signature/method/function declaration, they will be recorded here.
	CommentLines []string
}

// Built in types.
var (
	String = &Type{
		Name: Name{Name: "string"},
		Kind: Builtin,
	}
	Int64 = &Type{
		Name: Name{Name: "int64"},
		Kind: Builtin,
	}
	Int32 = &Type{
		Name: Name{Name: "int32"},
		Kind: Builtin,
	}
	Int16 = &Type{
		Name: Name{Name: "int16"},
		Kind: Builtin,
	}
	Int = &Type{
		Name: Name{Name: "int"},
		Kind: Builtin,
	}
	Uint64 = &Type{
		Name: Name{Name: "uint64"},
		Kind: Builtin,
	}
	Uint32 = &Type{
		Name: Name{Name: "uint32"},
		Kind: Builtin,
	}
	Uint16 = &Type{
		Name: Name{Name: "uint16"},
		Kind: Builtin,
	}
	Uint = &Type{
		Name: Name{Name: "uint"},
		Kind: Builtin,
	}
	Uintptr = &Type{
		Name: Name{Name: "uintptr"},
		Kind: Builtin,
	}
	Float64 = &Type{
		Name: Name{Name: "float64"},
		Kind: Builtin,
	}
	Float32 = &Type{
		Name: Name{Name: "float32"},
		Kind: Builtin,
	}
	Float = &Type{
		Name: Name{Name: "float"},
		Kind: Builtin,
	}
	Bool = &Type{
		Name: Name{Name: "bool"},
		Kind: Builtin,
	}
	Byte = &Type{
		Name: Name{Name: "byte"},
		Kind: Builtin,
	}

	builtins = &Package{
		Types: map[string]*Type{
			"bool":    Bool,
			"string":  String,
			"int":     Int,
			"int64":   Int64,
			"int32":   Int32,
			"int16":   Int16,
			"int8":    Byte,
			"uint":    Uint,
			"uint64":  Uint64,
			"uint32":  Uint32,
			"uint16":  Uint16,
			"uint8":   Byte,
			"uintptr": Uintptr,
			"byte":    Byte,
			"float":   Float,
			"float64": Float64,
			"float32": Float32,
		},
		Imports: map[string]*Package{},
		Path:    "",
		Name:    "",
	}
)

// IsInteger is
func IsInteger(t *Type) bool {
	switch t {
	case Int, Int64, Int32, Int16, Uint, Uint64, Uint32, Uint16, Byte:
		return true
	default:
		return false
	}
}
